#include "Renderizable.h"

#include <glm\gtc\matrix_inverse.hpp>
#include <glm\gtc\matrix_transform.hpp>


Renderizable::Renderizable() : 
	m_dephTrans(0.0f),
	m_shininess(1.0f),	// 1.0f == superficie mate
	m_coefDifuso(0.0f, 0.0f, 0.0f),
	m_coefAmbiente(0.0f, 0.0f, 0.0f),
	m_coefEspecular(0.0f, 0.0f, 0.0f),
	m_dirLuzDireccional(1.0f, 1.0f, 10.0f)
{
}

Renderizable::~Renderizable() { }


void Renderizable::init(const glm::vec3 &direccionLuz, std::string vertShaderFileName, std::string fragShaderFileName) {
	// Es conveniente guardar la direccion de la luz direccional
	this->m_dirLuzDireccional = direccionLuz;

	// NO MODIFICAR, esta hardcodeado para que funcione el shadow map estatico
	// Se calcula la matriz de profundidad para los calculos del shadowMap
	glm::mat4 viewDeph = glm::lookAt( glm::vec3(20.0f, 30.0f, 20.0f), glm::vec3(0.0f, 0.0f, 0.1f), glm::vec3(0.0f, 0.0f, 1.0f) );
	glm::mat4 projecDeph = glm::ortho(-20.0f, 25.0f, -11.25f, 22.5f, 0.0f, 100.0f);

	// Es para aplicar una correccion al mapeo de texturas
	glm::mat4 biasDeph(
		0.5, 0.0, 0.0, 0.0,
		0.0, 0.5, 0.0, 0.0,
		0.0, 0.0, 0.5, 0.0,
		0.5, 0.5, 0.5, 1.0
	);
	this->m_dephTrans = biasDeph * projecDeph * viewDeph;
	this->m_shadows.cargarMapaSombras("ShadowMap_MKVI.dds");

	// Se inicializa el shaderLoader
	this->m_shaders.init();

	// Se cargan los shaders
	bool exito = this->m_shaders.loadVertexShader(vertShaderFileName);
	if (!exito) throw std::runtime_error("Falló la carga del vertex shader\n" + this->m_shaders.getErrorMsg());

	exito = this->m_shaders.loadFragmentShader(fragShaderFileName);
	if (!exito) throw std::runtime_error("Falló la carga del fragment shader\n" + this->m_shaders.getErrorMsg());

	// Se linkean y validan los shaders
	exito = this->m_shaders.compilarShaders();
	if (!exito) throw std::runtime_error("Falló el linkeo de los shaders\n" + this->m_shaders.getErrorMsg());
}


void Renderizable::setMaterial(const glm::vec3 &coefDifusion, const glm::vec3 &coefAmbiente, const glm::vec3 &coefEspecular, float shininess) {
	this->m_coefDifuso = coefDifusion;
	this->m_coefAmbiente = coefAmbiente;
	this->m_coefEspecular = coefEspecular;
	
	if (shininess > 1.0f){
		this->m_shininess = shininess;
	}
}


// Setear la textura del elemento, se hace una sola vez
void Renderizable::setTextura(const std::string filename){
	const GLuint textura = this->m_textura.loadTexture(filename);

	if (textura <= 0) throw std::runtime_error("Error al setear la textura.\n");
}


GLuint Renderizable::cargarShaderProgram() const {
	GLuint shaderProgramID = this->m_shaders.getProgramID();
	glUseProgram(shaderProgramID);

	return shaderProgramID;
}


void Renderizable::cargarMatricesTransformacion(const glm::mat4 &viewMat, const glm::mat4 &projectionMat, const glm::mat4 &modelMat) const {
	// Obtengo el shader program
	GLuint shaderProgramID = this->m_shaders.getProgramID();

	// Primero se obtiene la "location" de cada matriz, todas deben estar definidas en el shader
	const GLint locModelMatrix = glGetUniformLocation(shaderProgramID, "modelMatrix");
	if (locModelMatrix == -1) throw std::runtime_error("No existe el parametro 'modelMatrix' en el shader");

	const GLint locViewMatrix = glGetUniformLocation(shaderProgramID, "viewMatrix");
	if (locViewMatrix == -1) throw std::runtime_error("No existe el parametro 'viewMatrix' en el shader");

	const GLint locProjMatrix = glGetUniformLocation(shaderProgramID, "projectionMatrix");
	if (locProjMatrix == -1) throw std::runtime_error("No existe el parametro 'projectionMatrix' en el shader");

	// Luego se las carga en el shader
	glUniformMatrix4fv(locModelMatrix, 1, GL_FALSE, &modelMat[0][0]);
	glUniformMatrix4fv(locViewMatrix, 1, GL_FALSE, &viewMat[0][0]);
	glUniformMatrix4fv(locProjMatrix, 1, GL_FALSE, &projectionMat[0][0]);
}


void Renderizable::cargarDatosIluminacion(const glm::mat4 &matModel, const glm::mat4 &matView, float alpha) const {
	// Obtengo el shader program
	GLuint shaderProgramID = this->m_shaders.getProgramID();

	// Primero obtiene los datos de los elementos de iluminacion
	const GLint locDirLuz = glGetUniformLocation(shaderProgramID, "direccionLuz");
	const GLint locShininess = glGetUniformLocation(shaderProgramID, "Shininess");
	const GLint locShadowDeph = glGetUniformLocation(shaderProgramID, "shadowDephMVP");
	const GLint locShadowMaps = glGetUniformLocation(shaderProgramID, "shadowMap");

	const GLint locAlpha = glGetUniformLocation(shaderProgramID, "alpha");
	const GLint locCoefDifusa = glGetUniformLocation(shaderProgramID, "Kd");
	const GLint locCoefAmbiente = glGetUniformLocation(shaderProgramID, "Ka");
	const GLint locCoefEspecular = glGetUniformLocation(shaderProgramID, "Ke");

	// Los carga en el shader; No todos tienen que estar definidos
	if (locDirLuz != -1) glUniform3fv(locDirLuz, 1, &this->m_dirLuzDireccional[0]);
	if (locShininess != -1) glUniform1f(locShininess, this->m_shininess);

	// Aca se carga el shadow map en el shader
	if (locShadowDeph != -1 && locShadowMaps != -1){
		glm::mat4 dephtMat = this->m_dephTrans * matModel;
		this->m_shadows.sendShadowMap(locShadowDeph, locShadowMaps, dephtMat);
	}

	if (locAlpha != -1) glUniform1f(locAlpha, alpha);
	if (locCoefDifusa != -1) glUniform3fv(locCoefDifusa, 1, &this->m_coefDifuso[0]);
	if (locCoefAmbiente != -1) glUniform3fv(locCoefAmbiente, 1, &this->m_coefAmbiente[0]);
	if (locCoefEspecular != -1) glUniform3fv(locCoefEspecular, 1, &this->m_coefEspecular[0]);

	// La matriz de normales para la iluminacion
	const GLint locNormalMat = glGetUniformLocation(shaderProgramID, "normalMatrix");
	if (locNormalMat != -1){
		// No la calcula si no es necesario, es la inversa transpuesta de ModelView Matrix
		glm::mat3 matNormales = glm::inverseTranspose( glm::mat3(matView * matModel) );
		glUniformMatrix3fv(locNormalMat, 1, GL_FALSE, &matNormales[0][0]);
	}
}


void Renderizable::cargarTextura() const {
	// Obtengo el shader program
	const GLuint shaderProgramID = this->m_shaders.getProgramID();

	// Asocio el id de la textura a la variable Text1 del shader
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, this->m_textura.getTextureID());

	// Pasa los datos de la unidad de textura al shader
	const GLint loc = glGetUniformLocation(shaderProgramID, "textura");

	// Por simplicidad siempre usamos la misma unidad de textura, la "0"
	if( loc != -1 ) glUniform1i(loc, 0);
}


bool Renderizable::isTexturable() const {
	return this->m_textura.isLoaded();
}


// Setear la textura que se usa como mapa de normales, similar a setTextura pero en la unidad 1 de textura
void Renderizable::setNormalMap(const std::string filename){
	const GLuint normalmap = this->m_textura.loadNormalMap(filename);

	if (normalmap <= 0) throw std::runtime_error("Error al setear el normal map.\n");
}

void Renderizable::cargarNormalMap() const {
	// Obtengo el shader program
	const GLuint shaderProgramID = this->m_shaders.getProgramID();

	// Asocio el id de la textura a la variable Text1 del shader
	glActiveTexture(GL_TEXTURE1);
	glBindTexture(GL_TEXTURE_2D, this->m_textura.getNormalMapID());

	// Pasa los datos de la unidad de textura al shader
	const GLint loc = glGetUniformLocation(shaderProgramID, "NormalMap");

	// Por simplicidad siempre usamos la misma unidad de textura, la "1"
	if( loc != -1 ) glUniform1i(loc, 1);
}

void Renderizable::setCubeMap(){
	const GLuint cubemap = this->m_textura.loadCubeMap();

	if (cubemap <= 0) throw std::runtime_error("Error al setear el cube map.\n");
}

void Renderizable::cargarCubeMap() const {
	// Obtengo el shader program
	const GLuint shaderProgramID = this->m_shaders.getProgramID();

	// Asocio el id de la textura a la variable CubeMap del shader
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_CUBE_MAP, this->m_textura.getCubeMapID());

	// Pasa los datos de la unidad de textura al shader
	const GLint loc = glGetUniformLocation(shaderProgramID, "CubeMap");

	// Por simplicidad siempre usamos la misma unidad de textura, la "0"
	if( loc != -1 ) glUniform1i(loc, 0);
}
